#!/usr/bin/env perl
use strict;
use warnings;
use warnings FATAL => 'uninitialized';

use tool::Util;

use Test::More 0.88;

main(@ARGV);
exit;

sub difflet {
    my($got, $expected) = @_;
    require Data::Difflet;
    my $d = Data::Difflet->new();
    note("DIFFLET");
    note($d->compare($got, $expected));
}

sub node_supports_generator {
    my $node = $ENV{JSX_RUNJS} || "node";
    open my $fh, "-|", "$node --version"
        or die "failed to invoke node:$!";
    my $output = do { local $/; <$fh> };
    close $fh;
    die "node --version exitted abnormally:$?"
        if $? != 0;

    return $output =~ /v0.([0-9]+)/ && $1 >= 11;
}

sub bench {
    my ($f) = @_;
    require Time::HiRes;
    my $start = [Time::HiRes::gettimeofday()];
    my $ret = $f->();
    return ($ret, Time::HiRes::tv_interval($start));
}

sub main {
    my($file) = @_ or die "no args";

    local $TODO = 'marked as TODO' if ($file =~ /\b todo \b/xms);

    if(my @shebang = get_shebang($file)) {
        exec @shebang, $file;
    }
    elsif($file =~ m{ /run/ }xms) {
        compile_and_run($file);
    }
    elsif($file =~ m{ /compile_error/ }xms) {
        expect_compile_error($file);
    }
    elsif($file =~ m{ /lib/ }xms) {
        run_test($file);
    }
    elsif($file =~ m{ /src/ }xms) {
        run_test($file);
    }
    elsif($file =~ m{ /web/ }xms) {
        run_web_test($file);
    }
    elsif($file =~ m{ /optimize-bench/ }xms) {
        optimize_and_compare($file, 1);
    }
    elsif($file =~ m{ /optimize/ }xms) {
        optimize_and_compare($file, 0);
    }
    elsif($file =~ m{ /complete/ }xms) {
        completion_test($file);
    }
    else {
        plan tests => 1;
        fail $file;
    }
}

sub get_shebang {
    my($file) = @_;

    open my($fh), "<", $file or die "Cannot open $file for reading: $!";
    my $first = <$fh>;
    close $fh;

    return grep { defined } $first =~ /\A \#\! \s* (\S+) (?: \s+ (\S+) )* /xmsg;
}

sub run_compiled {
    my ($filename, $src, $node_opts) = @_;

    $node_opts ||= '';

    require File::Temp;

    # add the bootstrap code
    $src .= sprintf <<'EOT', $filename;
;
// workaround for node.js to set "JSX" to global
Function("return this")().JSX = JSX;
// invoke the test
try {
    JSX.require("%s")._Main.main([]);
} catch (e) {
    console.log(e.message.replace(/^\[.*?\]\s*/, ""));
}
EOT

    # write to temp file
    my $temp = File::Temp->new(SUFFIX => ".js");
    print $temp $src;
    close $temp;

    # execute compiled node
    my $js = $ENV{JSX_RUNJS} || "node";
    open my $fh, "-|", "$js $node_opts $temp"
        or die "failed to invoke node:$!";
    local $/;
    my $output = <$fh>;
    close $fh;

    return if $? != 0;

    return $output;
}

sub compile_and_run {
    my($file) = @_;

    my $opts = get_section($file, "JSX_OPTS");
    if (defined $opts) {
        chomp $opts;
        plan skip_all => 'environment variable JSX_OPTS is set'
            if $ENV{JSX_OPTS};
    }

    defined(my $expected = get_section($file, "EXPECTED"))
        or die "could not find EXPECTED in file:$file\n";
    $expected =~ s/\s+$//;

    my $jssetup = get_section($file, "JS_SETUP") || "";

    $opts ||= "";

    if ($file =~ m{/generator\.}) {
        plan tests => 2;
        SKIP: {
            skip "skipping native generator test (node does not support harmony)", 1
                unless node_supports_generator();
            _compile_and_run("$file (native generator)", $file, "--harmony", $opts, $jssetup, $expected, 1);
        }
        _compile_and_run("$file (generator emulation)", $file, "", "$opts --enable-generator-emulation", $jssetup, $expected, 0);
    } else {
        plan tests => 1;
        _compile_and_run($file, $file, "", $opts, $jssetup, $expected, 0);
    }
}

sub _compile_and_run {
    my ($name, $file, $node_opts, $opts, $jssetup, $expected) = @_;

    # compile (FIXME support C++)
    my($ok, $src, $logs) = jsx("$opts $file");
    if (! $ok) {
        fail "failed to compile '$file'";
        diag $logs;
        return;
    };
    # run
    defined (my $output = run_compiled($file, $jssetup . $src, $node_opts)) or do {
        fail "failed to execute compiled script";
        return;
    };
    $output   =~ s/\s+$//;

    # compare the results
    is $output, $expected, $name or do {
        difflet([split /\n/, $output], [split /\n/, $expected]);
    };
}

sub optimize_and_compare {
    my ($file, $do_bench) = @_;

    plan skip_all => 'environment variable JSX_OPTS is set'
        if defined $ENV{JSX_OPTS};

    defined(my $expected = get_section($file, "EXPECTED"))
        or die "could not find EXPECTED in file:$file\n";
    defined(my $opts = get_section($file, "JSX_OPTS"))
        or die "cloud not find JSX_OPTS in file:$file\n";

    my $bench_least_ratio;
    if ($do_bench) {
        $bench_least_ratio = get_section($file, "BENCHMARK");
        chomp $bench_least_ratio;
        $bench_least_ratio ||= 1;
    }

    chomp $opts;

    my @optimization_flags = map { split /,/ } ($opts =~ /--optimize \s+ (\S+)/xmsg);

    plan tests => 6 + ($do_bench ? 1 : 0) + scalar(@optimization_flags);

    # compile, run, and check (wo. optimization)
    my($ok, $src, $logs) = jsx("--warn", "none", $file);
    ok $ok, "compile '$file' (wo. optimization)" or return diag($logs);
    is $logs, '', "... with no logs";
    my ($output, $elapsed) = bench(sub { run_compiled($file, $src) });

    is $output, $expected,
        "output of '$file' (wo. optimization)" or return;

    # compile, run, and check (w. optimization)
    ($ok, my $src_optimized, my $logs_optimized) = jsx("$opts --optimize dump-logs $file");
    ok $ok, "compile '$file' (w. $opts)" or return diag($logs);
    for my $flag(@optimization_flags) {
        like $logs_optimized, qr/\[ \Q$flag\E \]/xms, "... with [$flag] in logs";
    }


    ($output, my $elapsed_optimized) = bench(sub { run_compiled($file, $src_optimized) });

    is $output, $expected,
        "output of '$file' (w. $opts)" or return;

    isnt $src, $src_optimized, "generated code must be different";

    # check the times
    if ($do_bench) {
        if ($elapsed < 0.5) {
            fail "$file (unoptimized) exited too early ($elapsed seconds)";
            return;
        }
        if ($elapsed_optimized < 0.5) {
            fail "$file (optimized) exited too early ($elapsed_optimized seconds)";
            return;
        }
        my $ratio = sprintf '%.03f', ($elapsed / $elapsed_optimized) * 100 - 100;
        cmp_ok $ratio, '>=', $bench_least_ratio, "$file - at least $bench_least_ratio% faster by $opts";
        diag "$file - $ratio\% faster by optimization";
    }
}

sub expect_compile_error {
    my($file) = @_;

    my $opts = get_section($file, "JSX_OPTS");
    if (defined $opts) {
        chomp $opts;
        plan skip_all => 'environment variable JSX_OPTS is set'
            if $ENV{JSX_OPTS};
    }
    $opts ||= "";

    plan tests => 7;

    require POSIX;

    my($ok, $stdout, $stderr, $status) = jsx("$opts $file");
    ok !$ok, "failed to compile (jsx $opts $file)";
    ok POSIX::WIFEXITED($status), "exit status should be normal";
    is POSIX::WEXITSTATUS($status), 65, "exit status should be EX_DATAERR";
    # checks below might be covered by the exit status test right above
    unlike $stderr, qr/^ \s+ \b at \b \s+ \b Module \b/xms,
        "... without compiler crash";
    unlike $stderr, qr/\b logic \s+ flaw \b/xms,
        "... without logic flaw";
    unlike $stderr, qr/\b assertion \s+ failure \b/xms,
        "... without assertion failure";
    unlike $stderr, qr/^ TypeError:/xms,
        "... without TypeError";
    note $stderr;
}

sub completion_test {
    my($file) = @_;

    plan skip_all => 'environment variable JSX_OPTS is set'
        if defined $ENV{JSX_OPTS};

    plan tests => 2;

    defined(my $expected = get_section($file, "EXPECTED"))
        or die "could not find EXPECTED in file:$file\n";
    defined(my $opts = get_section($file, "JSX_OPTS"))
        or die "could not find JSX_OPTS in file:$file\n";

    chomp $opts;

    # run and check the output
    my($ok, $stdout, $stderr) = jsx("$opts $file");
    if (ok $ok, "compile $file") {
        require JSON;
        my $output_data   = JSON::decode_json($stdout);
        my $expected_data = JSON::decode_json($expected);

        my $to_test = [
            map {
                my $data = {};
                for my $field(qw(word partialWord type returnType args definedClass)) {
                    $data->{$field} = $_->{$field} if exists $_->{$field} and $_->{$field};
                }
                $data;
            } @{$output_data}
        ];

        is_deeply $to_test, $expected_data, "check completion candidates of $file" or difflet($to_test, $expected_data);
    }
    else {
        fail "Cannot compile jsx $opts $file";
        diag $stderr;
    }
}

sub run_test {
    my($file) = @_;

    my($ok, $stdout, $stderr) = jsx("--test $file");

    if ($ok) {
        print STDOUT $stdout;
    }
    else {
        fail "Cannot run jsx --test $file";
        diag $stderr;
    }
}

sub run_web_test {
    my($file) = @_;

    require File::Which;
    my $phantomjs = File::Which::which("phantomjs");
    if (! $phantomjs ) {
        local $TODO = "no phantomjs(1)";
        fail $file;
        done_testing;
        return;
    }
    chomp $phantomjs;

    my $phantomjs_ver = `$phantomjs --version`;
    chomp $phantomjs_ver;
    if (numify_version($phantomjs_ver) < numify_version("1.8.0")) {
        local $TODO = "phantomjs $phantomjs_ver is too old";
        fail $file;
        done_testing;
        return;
    }
    note "phantomjs $phantomjs_ver";

    if ($ENV{SKIP_PHANTOMJS_TEST}) {
        pass "skipped";
        done_testing;
        return;
    }

    local $ENV{JSX_RUNJS} = $phantomjs;

    local $SIG{ALRM} = sub { die @_ };
    alarm 60;

    my($ok, $stdout, $stderr) = eval {
        jsx("--executable commonjs --test $file");
    };
    alarm 0; # reset
    if ($ok) {
        print STDOUT $stdout;
    }
    else {
        fail("Cannot run jsx --test $file with $phantomjs $phantomjs_ver");
        diag $@, $stderr;
    }
}
# vim: set expandtab:
